summaryrefslogtreecommitdiff
path: root/app/api/auth/[...nextauth]/saml/provider.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/auth/[...nextauth]/saml/provider.ts')
-rw-r--r--app/api/auth/[...nextauth]/saml/provider.ts259
1 files changed, 259 insertions, 0 deletions
diff --git a/app/api/auth/[...nextauth]/saml/provider.ts b/app/api/auth/[...nextauth]/saml/provider.ts
new file mode 100644
index 00000000..8486a690
--- /dev/null
+++ b/app/api/auth/[...nextauth]/saml/provider.ts
@@ -0,0 +1,259 @@
+import CredentialsProvider from "next-auth/providers/credentials"
+import { getOrCreateSAMLUser, validateSAMLUserData } from '@/lib/users/saml-service'
+import { encode } from 'next-auth/jwt'
+import type { User } from 'next-auth'
+import type { SAMLUser } from './utils'
+import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils'
+
+interface SAMLProviderOptions {
+ id: string
+ name: string
+ idp: {
+ sso_login_url: string
+ sso_logout_url: string
+ certificates: string[]
+ }
+ sp: {
+ entity_id: string
+ private_key: string
+ certificate: string
+ assert_endpoint: string
+ }
+}
+
+export function SAMLProvider(options: SAMLProviderOptions) {
+ return CredentialsProvider({
+ id: options.id,
+ name: options.name,
+ credentials: {
+ user: {
+ label: "User Data",
+ type: "text"
+ }
+ },
+ async authorize(credentials) {
+ debugLog('๐Ÿ” SAMLProvider.authorize called with credentials:', credentials);
+
+ try {
+ debugLog('๐Ÿ” Checking credentials.user:', {
+ hasCredentials: !!credentials,
+ hasUser: !!credentials?.user,
+ userType: typeof credentials?.user,
+ userValue: credentials?.user?.substring?.(0, 100) + '...'
+ });
+
+ if (!credentials?.user) {
+ debugError('No user data provided in credentials')
+ return null
+ }
+
+ debugProcess('SAML Provider: Processing user data')
+
+ // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ (UTF-8 ์ฒ˜๋ฆฌ ๊ฐœ์„ )
+ const userDataString = credentials.user
+ debugLog('๐Ÿ”ค Raw user data string:', userDataString.substring(0, 200) + '...')
+
+ let userData;
+ try {
+ userData = JSON.parse(userDataString);
+ debugSuccess('JSON parsing successful:', userData);
+ } catch (parseError) {
+ debugError('JSON parsing failed:', parseError);
+ debugError('Raw string that failed to parse:', userDataString);
+ return null;
+ }
+
+ // ํŒŒ์‹ฑ๋œ ๋ฐ์ดํ„ฐ์˜ UTF-8 ํ™•์ธ
+ debugLog('๐Ÿ”ค Parsed user data UTF-8 check:', {
+ name: userData.name,
+ nameLength: userData.name?.length,
+ charCodes: userData.name ? [...userData.name].map(c => c.charCodeAt(0)) : []
+ })
+
+ if (!userData.id || !userData.email) {
+ debugError('Invalid SAML user data:', userData)
+ return null
+ }
+
+ debugSuccess('SAML Provider: User authenticated successfully', {
+ id: userData.id,
+ email: userData.email,
+ name: userData.name
+ })
+
+ // ๐Ÿ”ฅ SAML ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
+ debugProcess('Validating SAML user data structure...');
+ const isValidData = await validateSAMLUserData(userData)
+ debugLog('Validation result:', isValidData);
+ if (!isValidData) {
+ debugError('Invalid SAML user data structure:', userData)
+ return null
+ }
+
+ // ๐Ÿ”ฅ JIT (Just-In-Time) ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋˜๋Š” ์กฐํšŒ
+ debugProcess('Creating/getting SAML user from database...');
+ const userCreateData = {
+ email: userData.email,
+ name: userData.name,
+ companyId: undefined,
+ techCompanyId: undefined,
+ domain: userData.domain
+ };
+ debugLog('User create data:', userCreateData);
+
+ const dbUser = await getOrCreateSAMLUser(userCreateData)
+ debugLog('Database user result:', dbUser);
+
+ if (!dbUser) {
+ debugError('Failed to get or create SAML user')
+ return null
+ }
+
+ // DB์—์„œ ๊ฐ€์ ธ์˜จ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ˜ํ™˜
+ const userResult = {
+ id: String(dbUser.id), // DB์˜ ์‹ค์ œ ID
+ name: dbUser.name, // DB์˜ ์‹ค์ œ ์ด๋ฆ„
+ email: dbUser.email, // DB์˜ ์‹ค์ œ ์ด๋ฉ”์ผ
+ companyId: dbUser.companyId, // DB์˜ ์‹ค์ œ ํšŒ์‚ฌ ID
+ techCompanyId: dbUser.techCompanyId, // DB์˜ ์‹ค์ œ ๊ธฐ์ˆ ํšŒ์‚ฌ ID
+ domain: dbUser.domain, // DB์˜ ์‹ค์ œ ๋„๋ฉ”์ธ
+ imageUrl: dbUser.imageUrl, // DB์˜ ์‹ค์ œ ์ด๋ฏธ์ง€ URL
+ }
+
+ debugSuccess('SAML Provider: Returning user data to NextAuth:', userResult)
+ return userResult
+ } catch (error) {
+ debugError('SAML Provider: Authentication failed', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ errorType: typeof error,
+ credentials: credentials
+ });
+ return null
+ }
+ }
+ })
+}
+
+// SAML ๋กœ๊ทธ์ธ URL ์ƒ์„ฑ ํ—ฌํผ ํ•จ์ˆ˜
+export function getSAMLLoginUrl(options: SAMLProviderOptions): string {
+ const params = new URLSearchParams({
+ SAMLRequest: 'placeholder', // ์‹ค์ œ๋กœ๋Š” createAuthnRequest()๋กœ ์ƒ์„ฑ
+ RelayState: options.sp.assert_endpoint,
+ })
+
+ return `${options.idp.sso_login_url}?${params.toString()}`
+}
+
+// SAML ์„ค์ • ๊ฒ€์ฆ
+export function validateSAMLOptions(options: SAMLProviderOptions): boolean {
+ const required = [
+ options.idp.sso_login_url,
+ options.sp.entity_id,
+ options.sp.assert_endpoint
+ ]
+
+ return required.every(field => field && field.length > 0)
+}
+
+// SAMLProvider์˜ authorize ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•œ ํ—ฌํผ
+export async function authenticateSAMLUser(userData: SAMLUser) {
+ debugLog('authenticateSAMLUser called with:', userData);
+
+ try {
+ // SAMLProvider ๋Œ€์‹  ์ง์ ‘ ๋กœ์ง ์‹คํ–‰ (Provider ๋ž˜ํผ ์—†์ด)
+ debugProcess('SAML User Authentication: Processing user data')
+
+ // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
+ if (!userData.id || !userData.email) {
+ debugError('Invalid SAML user data:', userData)
+ return null
+ }
+
+ debugSuccess('SAML User data validated successfully', {
+ id: userData.id,
+ email: userData.email,
+ name: userData.name
+ })
+
+ // ๐Ÿ”ฅ SAML ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
+ debugLog('Validating SAML user data structure...');
+ const isValidData = await validateSAMLUserData(userData)
+ debugLog('Validation result:', isValidData);
+ if (!isValidData) {
+ debugError('Invalid SAML user data structure:', userData)
+ return null
+ }
+
+ // ๐Ÿ”ฅ JIT (Just-In-Time) ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋˜๋Š” ์กฐํšŒ
+ debugLog('Creating/getting SAML user from database...');
+ const userCreateData = {
+ email: userData.email,
+ name: userData.name,
+ companyId: undefined,
+ techCompanyId: undefined,
+ domain: userData.domain
+ };
+ debugLog('User create data:', userCreateData);
+
+ const dbUser = await getOrCreateSAMLUser(userCreateData)
+ debugLog('Database user result:', dbUser);
+
+ if (!dbUser) {
+ debugError('Failed to get or create SAML user')
+ return null
+ }
+
+ // DB์—์„œ ๊ฐ€์ ธ์˜จ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ˜ํ™˜
+ const userResult = {
+ id: String(dbUser.id), // DB์˜ ์‹ค์ œ ID
+ name: dbUser.name, // DB์˜ ์‹ค์ œ ์ด๋ฆ„
+ email: dbUser.email, // DB์˜ ์‹ค์ œ ์ด๋ฉ”์ผ
+ companyId: dbUser.companyId, // DB์˜ ์‹ค์ œ ํšŒ์‚ฌ ID
+ techCompanyId: dbUser.techCompanyId, // DB์˜ ์‹ค์ œ ๊ธฐ์ˆ ํšŒ์‚ฌ ID
+ domain: dbUser.domain, // DB์˜ ์‹ค์ œ ๋„๋ฉ”์ธ
+ imageUrl: dbUser.imageUrl, // DB์˜ ์‹ค์ œ ์ด๋ฏธ์ง€ URL
+ }
+
+ debugSuccess('SAML User Authentication completed:', userResult)
+ return userResult;
+
+ } catch (error) {
+ debugError('authenticateSAMLUser error:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ userData
+ });
+ return null;
+ }
+}
+
+// NextAuth JWT ํ† ํฐ ์ƒ์„ฑ ํ—ฌํผ
+export async function createNextAuthToken(user: User): Promise<string> {
+ const token = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ companyId: user.companyId,
+ techCompanyId: user.techCompanyId,
+ domain: user.domain,
+ imageUrl: user.imageUrl,
+ iat: Math.floor(Date.now() / 1000),
+ exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30์ผ
+ };
+
+ const secret = process.env.NEXTAUTH_SECRET!;
+ return await encode({ token, secret });
+}
+
+// NextAuth ์„ธ์…˜ ์ฟ ํ‚ค ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ
+export function getSessionCookieName(): string {
+ // NEXTAUTH_URL์ด HTTPS์ธ ๊ฒฝ์šฐ์—๋งŒ __Secure- ์ ‘๋‘์‚ฌ ์‚ฌ์šฉ
+ const nextAuthUrl = process.env.NEXTAUTH_URL || '';
+ const isHttps = nextAuthUrl.startsWith('https://');
+
+ return isHttps
+ ? '__Secure-next-auth.session-token'
+ : 'next-auth.session-token';
+}
+ \ No newline at end of file